﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using Common;
using System.Diagnostics;


namespace SIGSender
{
   
    
    /// <summary>
    /// Class that implements the SIG sender protocol
    /// </summary>
    public class SelectiveRepeat
    {                
        private long _packetsToACK, _packetsToQ;
        private int _seqNo = 1;
        private MemoryStream _toSendStream;
        private bool timedOut = false;
        private Queue<WindowElement> _window = new Queue<WindowElement>();
        private Timer _globalTimer;
        private int _globalCounter, _packetTimeout, _globalTimeout;        
        private int _windowSize = 0;
        UdpClient _client, _incomingConnection;
        Mutex _mutex = new Mutex();

        

        UdpState s;
        
        IPEndPoint _ep = null;        

        BinaryFormatter _serializer = new BinaryFormatter();

        /// <summary>
        /// Creates an instance of the SIG protocol
        /// </summary>
        /// <param name="serverName">Name (or IP) of server to connect to</param>
        /// <param name="remotePort">Port on server to connect to</param>
        public SelectiveRepeat(String serverName, Int32 remotePort)
        {
            try
            {                  
                _client = new UdpClient(serverName, remotePort);
                _incomingConnection = new UdpClient(serverName, remotePort + 1);                

                _ep = (IPEndPoint)_incomingConnection.Client.LocalEndPoint;

                s = new UdpState();
                s.e = _ep;
                s.u = _incomingConnection;

                
                SIGPacket packet = new SIGPacket();
                packet.flags = Flags.SYN_ACK;
                //Initialize the incoming connection
                sendPacket(packet, _incomingConnection);

                //Console.WriteLine("listening for messages");                
                _incomingConnection.BeginReceive(new AsyncCallback(UDPReceiveCallback), s);
                
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            _packetTimeout = 1;
            _globalTimeout = 20;
            _windowSize = 5;
        }

        /// <summary>
        /// Sends a buffer over the network.
        /// </summary>
        /// <param name="buffer">The buffer to send</param>
        /// <returns>Number of bytes sent</returns>
        public int sendData(byte[] buffer)
        {
            _toSendStream = new MemoryStream(buffer);            
            
            _packetsToQ =  ((int)Math.Ceiling(buffer.Length / (double)Packetizer.MAX_MTU)) ;
            _packetsToACK = _packetsToQ + 1;
            //Send packets initially
            sendPackets();
            _globalTimer = new Timer(TimerCallback, s, 1000, 1000);                       

            

            while (!timedOut && _packetsToACK > 0)
            {
                //Nothing to do, it is all taken care of by the UDP handler and the timer handler.
                Thread.Sleep(100);
            }

            return 0;
        }
       

        private void UDPReceiveCallback(IAsyncResult ar)
        {
            Console.WriteLine("UDP Callback triggered");

            _globalCounter = 0;

            _ep = ((UdpState)ar.AsyncState).e;
            UdpClient u = ((UdpState)ar.AsyncState).u;
            //TODO: Error checking ?
            
            Byte[] receiveBytes = u.EndReceive(ar, ref _ep);

           
            //TODO: should have a function for this
            SIGPacket packet = (SIGPacket)_serializer.Deserialize(new MemoryStream(receiveBytes)) ;
          

            if (packet.flags == Flags.ACK)
            {
                //Is the ACK number a valid one?

                Console.WriteLine("Packet " + packet.seqNo.ToString() + " acked");


                //TODO: Implement fast restransmit
                lock (_window)
                {
                    foreach (WindowElement el in _window.AsEnumerable())
                    {
                        if (packet.seqNo == el.packet.seqNo && el.acked == false)
                        {
                            el.acked = true;
                            el.timerEnabled = false;
                            //Remove the packet if it is the last one, and maybe more
                            //This might change the underlying data for the iterator and trigger an exception

                            lock (_window)
                            {
                                while (_window.Peek().acked)
                                {
                                    WindowElement elem = _window.Dequeue();
                                    Console.WriteLine("Packet " + elem.packet.seqNo.ToString() + " dequeued");

                                    if (_window.Count == 0)
                                    {
                                        _packetsToACK = 0;
                                        _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);

                                        return;
                                    }
                                }
                            }

                            break;
                        }
                    }

                    if (_window.Count < this._windowSize)
                        sendPackets();


                }
            }
            //Re-enable receiving            
            s = new UdpState();
            s.e = new IPEndPoint(IPAddress.Any, 0);
            s.u = _incomingConnection;

            if (_window.Count > 0)
                _incomingConnection.BeginReceive(new AsyncCallback(UDPReceiveCallback), s);
            else
            {
                _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);                
            }

        }

        public void CloseConnection()
        {            
            //Send an ACK packet for the RST?
            _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
            _globalTimer.Dispose();

            //_client.EndReceive(null, ref _ep);
            _client.Close();
            
            
            //GC.Collect(2);
            //GC.WaitForPendingFinalizers();
        }

        public void TimerCallback(Object stateInfo)
        {
            Console.WriteLine("Timer Callback triggered at " + DateTime.Now.Second.ToString());
            _globalTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
            lock (_window)
            {
                foreach (WindowElement el in _window.AsEnumerable())
                {
                    if (el.timerEnabled)
                        el.incTimer();
                }
            }
            _globalCounter++;
            sendPackets();

            if (_packetsToACK >0)
                _globalTimer.Change(1000, 1000);

            if (_globalCounter == _globalTimeout)
            {
                Console.WriteLine("Connection timed out");
                CloseConnection();
            }
        }


        public int sendPackets()
        {
            int sentPackets = 0;
            lock (this)
            {
                //Add packets to send the queue
                while (_window.Count < _windowSize && _packetsToQ > 0)
                {
                    int bytesInPacket = (int)(_toSendStream.Length - _toSendStream.Position);
                    if (bytesInPacket > Packetizer.MAX_MTU)
                    {
                        bytesInPacket = Packetizer.MAX_MTU;
                    }

                    SIGPacket packet = new SIGPacket();
                    packet.seqNo = _seqNo;
                    _seqNo += 1;

                    packet.data = new byte[bytesInPacket];

                    //TODO: should do something with the returned value?
                    _toSendStream.Read(packet.data, 0, bytesInPacket);

                    WindowElement element = new WindowElement();
                    element.packet = packet;
                    element.timerEnabled = true;

                    _window.Enqueue(element);
                    _packetsToQ--;

                    //If all packets are ACKed, send the FIN packet
                    if (_packetsToQ == 0)
                    {
                        SIGPacket tpacket = new SIGPacket();
                        tpacket.flags = Flags.FIN;
                        WindowElement el = new WindowElement();
                        el.packet = tpacket;
                        el.timerEnabled = true;

                        _window.Enqueue(el);
                    }

                    Console.WriteLine("Packet " + packet.seqNo.ToString() + " added to queue");
                }



                //Send packets if timed out or ACKed
                lock (_window)
                {
                    foreach (WindowElement el in _window.AsEnumerable())
                    {
                        if ((!el.acked && el.getTimerCounter() >= _packetTimeout) ||
                             (el.sent == false)
                            )
                        {
                            if (el.getTimerCounter() >= _packetTimeout)
                                Console.WriteLine("Packet timed out");

                            sendElement(el);
                            sentPackets++;
                            Thread.Sleep(1);
                        }
                    }
                }
            }
            return sentPackets;
        }

        private void sendElement(WindowElement element)
        {
            SIGPacket packet = element.packet;

            sendPacket(packet, _client);

            element.sent = true;
            element.resetTimer();
            element.timerEnabled = true;
            Console.WriteLine("Packet " + element.packet.seqNo.ToString() + " sent");
        }

        private void sendPacket(SIGPacket packet, UdpClient client)
        {
            MemoryStream stream = new MemoryStream();
            _serializer.Serialize(stream, packet);

            byte[] sendBytes = stream.ToArray();
            
            client.Send(sendBytes, sendBytes.Length); 
        }
    }


 

}
